var Amqp = require('../amqp'),
    formatters = require('../tools/formatters');

var typeMap = { S: 'longstr', I: 'long', D: 'long', T: 'timestamp', F: 'table' };

function decoder(/**Buffer*/b)/**decoder*/{
    var b = b, p = 0, l = b.length;
    return {
        eof: function(){
            return p == l;
        },

        pointer: function(){
            return p;
        },

        end: function(size){
            return Amqp.Const.FrameEnd == b[p + size];
        },

        tail: function(/**Number*/n)/** Buffer */{
            if (n) return b.slice(p, p += n);
            var s = p;
            for (; p < l; ++p) {
                if (Amqp.Const.FrameEnd == b[p]) break;
            }
            return b.slice(s, p);
        },

        bit: function(){
            return b[p++];
        },

        char: function()/** String */{
            return b.slice(p, ++p).toString();
        },

        octet: function()/** Number */{
            return b[p++] << 8 * arguments[0] || 0;
        },

        short: function()/** Number */{
            return this.octet(1) + this.octet(0);
        },

        long: function()/** Number */{
            return this.octet(3) + this.octet(2) + this.octet(1) + this.octet(0);
        },

        longlong: function()/** Number */{
            return this.octet(7) + this.octet(6) + this.octet(5) + this.octet(4) + this.octet(3)
                    + this.octet(2) + this.octet(1) + this.octet(0);
        },

        shortstr: function()/** String */{
            var l = this.octet();
            return b.slice(p, p += l).toString();
        },

        longstr: function()/** String */{
            var l = this.long();
            return b.slice(p, p += l).toString();
        },

        timestamp: function(){
            return new Date(this.longlong());
        },

        table: function()/** Object */{
            var o = {}, l = p + this.long();
            for (; p < l;) {
                var key = this.shortstr(), type = this.char();
                if (type in typeMap) {
                    o[key] = this[typeMap[type]]();
                }
            }
            return o;
        }
    };
}

function method(/** reader */r)/** Object */{
    var a = {}, classId = r.short(), methodId = r.short(), A = Amqp.Class[classId][methodId];
    for (var i = 0, l = A.type.length; i < l; ++ i) {
        if (classId in formatters.map && methodId in formatters.map[classId] &&
                A.enum[i] in formatters.map[classId][methodId]) {
            a[A.enum[i]] = formatters.map[classId][methodId][A.enum[i]](r[A.type[i]]());
        } else {
            a[A.enum[i]] = r[A.type[i]]();
        }
    }
    return {
        classId: classId,
        methodId: methodId,
        arguments: a
    };
}

function header(/** reader */r)/** Object */{
    var classId = r.short(), weight = r.short(), size = r.longlong();
    var A = Amqp.Class[classId].fields;
    var flags, propertyFlags = [], propertyList = {};

    do {
        flags = r.short();
        for (var i = 15; i > 1; --i) {
            propertyFlags.push((Math.pow(2, i) & flags) ? 1 : 0);
        }
    } while(1 & flags);

    for (var i = 0, l = A.enum.length; i < l; ++i) {
        if (propertyFlags[i]) {
            propertyList[A.enum[i]] = r[A.type[i]]();
        }
    }

    return {
        size: size,
        weight: weight,
        fields: propertyList
    };
}

function part(/**decoder*/d,/**Object*/frame)/**Object*/{
    var type = d.octet(), channel = d.short(), size = d.long();

    if (!d.end(size)) {
        return d.pointer();
    }

    switch (type) {

    case Amqp.Const.FrameMethod:
    case Amqp.Const.FrameOobMethod:
        if ('method' in frame) var frame = {};
        frame.method = method(d);
        break;

    case Amqp.Const.FrameHeader:
    case Amqp.Const.FrameOobHeader:
        frame.header = header(d);
        if (frame.header.weight) {
            frame.childs = [];
            for (var i = frame.header.weight; i > -1; --i) {
                frame.childs.push(part(d, {}));
            }
        }
        break;

    case Amqp.Const.FrameBody:
    case Amqp.Const.FrameOobBody:
        frame.body = d.tail().toString();
        break;

    case Amqp.Const.FrameTrace:
    case Amqp.Const.FrameHeartbeat:
    }

    var frameEnd = d.octet();
//    if (Amqp.Const.FrameEnd != frameEnd) {
//        throw new Error('DECODING. Error frame end');
//    }

    if (!('channel' in frame)) {
        frame.channel = channel;
    }
    if (channel != frame.channel) {
        var sys = require('sys');
        throw new Error('Decoding\nError channel: expected <' + frame.channel + '>, received <' +
                channel + '>\n' + sys.inspect(frame, false, null));
    }

    return frame;
}

function decode(/**Buffer*/b)/**Object*/{
    var d = decoder(b), frame = {}, frames = [];
    for (var f; !d.eof();) {
        f = part(d, frame);
        if ('number' == typeof f) {
        }
        if (f !== frame) {
            frames.push(frame);
            frame = f;
        }
    }
    frames.push(frame);
    return frames;
};

exports.decode = decode;
